component accessors="true"
{
	property name="queuedPropertyResolvers" type="array";
	property name="queuedAggregates" type="array";
	
	property name="resolvedProperties" type="struct";
	property name="unresolvedPropertiesByName" type="struct";
	property name="unresolvedPropertiesByType" type="struct";
	property name="aggregatedTargetProperties" type="struct";
		
	property name="metadataBuilder";
	property name="validated" type="boolean";
		
	property name="mapper";
	property name="source";
	property name="target";
	property name="ignoredProperties" type="array";
	
	// Object, not dict, to allow MXML expression
	property name="aggregatedPropertyTargetTypes" type="struct";
		
	import com.firemoss.atonement.mapper.error.*;
	
	public function getResolvedPropertyMap()
	{
		return variables.resolvedProperties;
	}
	
	public function setPropertyResolvers( resolvers )
	{
		var resolver = "";
		
		for ( resolver in resolvers )
		{
			addPropertyResolver( resolver );
		}
	}
	

	
	public function init( source, target )
	{
		variables.source = getMetadata( source );
		variables.target = getMetadata( target );
		
		variables.queuedPropertyResolvers = [];
		variables.queuedAggregates = [];
		variables.resolvedProperties = {};
		variables.unresolvedPropertiesByName = {};
		variables.unresolvedPropertiesByType = {};
		variables.aggregatedTargetProperties = {};
		variables.ignoredProperties = [];
		variables.validated = false;
		
		return this;
	}
	
	private function dequeueDelayedConfiguration()
	{
		addQueuedPropertyResolvers();
		addQueuedAggregates();
	}
	
	private function addQueuedPropertyResolvers()
	{
		var resolver = "";
		
		while ( arrayLen( queuedPropertyResolvers ) > 0 )
		{
			resolver = queuedPropertyResolvers[ arrayLen( queuedPropertyResolvers ) ];
			arrayDeleteAt( queuedPropertyResolvers, arrayLen( queuedPropertyResolvers ) ) ;
			addPropertyResolver( resolver );
		}
	}
	
	private function addQueuedAggregates()
	{
		var args = "";
		
		while ( arrayLen( queuedAggregates ) > 0 )
		{
			args = queuedAggregates[ arrayLen( queuedAggregates ) ];
			arrayDeleteAt( queuedAggregates, arrayLen( queuedAggregates ) ) ;
			setAggregatedType( argumentCollection = args );
		}
	}	
	
	
	/**
	 * Adds mapping-based resolutions that have been registered with the assigned
	 * mapper _after_ this one.
	 */
	private function createLateMappedPropertyResolutions()
	{
		var unresolvedProperty = "";
		var key = "";
		
		if ( isNull( mapper ) )
		{
			throw new Error( "Can't resolve late mapped properties without a mapper!" );
		}

		for ( key in unresolvedPropertiesByType )
		{
			unresolvedProperty = unresolvedPropertiesByType[ key ];
			
			if ( mapper.mappingExistsBetweenByMetadata( unresolvedProperty.getSource(), unresolvedProperty.getTarget() ) )
			{
				var resolver = new PropertyResolver();
				resolver.setSourceProperty( unresolvedProperty.getSourceProperty().getName() );
				resolver.setTargetProperty( unresolvedProperty.getTargetProperty().getName() ); 	
				resolver.setValueResolutionTarget( createObject( "component", "com.firemoss.atonement.mapper.resolution.MappedValueResolver" ) );
				resolver.setValueResolutionFunction( "resolve" );
				
				addPropertyResolver( resolver );
			}
		}
	}	
	
		
	package function resolveProperties( resolverFactory )
	{
		var sourceProperties = metadataBuilder.buildMetadataForClass( source ).propertyMetadata;
		var targetProperties = metadataBuilder.buildMetadataForClass( target ).propertyMetadata;
		var targetProperty = "";
		var sourceProperty = "";
		var propertyName = "";
		
		for ( propertyName in targetProperties.getProperties() )
		{
			targetProperty = targetProperties.getProperties()[ propertyName ];
			
			// Don't overwrite explicitly resolved properties!
			if ( not structKeyExistsStronglyCased( resolvedProperties, targetProperty.getName() ) || !resolvedProperties[ targetProperty.getName() ].getExplicit() )
			{
				if ( sourceProperties.containsProperty( targetProperty.getName(), true ) )
				{
					var sourceProperty = sourceProperties.getProperty( targetProperty.getName() );
					
					if ( sourceProperty.getType() == targetProperty.getType() )
					{
						var resolver = new PropertyResolution();
							
						resolver.setSourceProperty( sourceProperty );
						resolver.setTargetProperty( targetProperty );
						resolver.setValueResolutionTarget( resolverFactory.create( sourceProperty.getType() ) );
						resolver.setValueResolutionFunction( "resolve" );
							
						resolvedProperties[ targetProperty.getName() ] = resolver;
					}
					else
					{
						var unresolvedByType = new UnresolvedTypeMapping();
						unresolvedByType.setSource( source );
						unresolvedByType.setTarget( target );
						unresolvedByType.setSourceProperty( sourceProperty );
						unresolvedByType.setTargetProperty( targetProperty );
						unresolvedPropertiesByType[ targetProperty.getName() ] = unresolvedByType;
					}
					
					// Is the target type an IList or Dictionary?  State that we need resolution of what to create when walking
					// the graph.
					if ( targetProperty.getType() eq "array" or targetProperty.getType() eq "struct" )
					{
						aggregatedTargetProperties[ targetProperty.getName() ] = targetProperty;
					}
				}
				else
				{
					var unresolvedByName = new UnresolvedNameMapping();
					unresolvedByName.setSource( source );
					unresolvedByName.setTarget( target );
					unresolvedByName.setTargetProperty( targetProperty );
					unresolvedPropertiesByName[ targetProperty.getName() ] = unresolvedByName;
				}
				
				// Post-processed metadata
				if ( targetProperty.getIgnored() )
				{
					ignoreProperty( targetProperty.getName() );
				}
				
				if ( len( targetProperty.getCollectionOf() ) )
				{
					setAggregatedType( targetProperty.getName(), targetProperty.getCollectionOf() );
				}
				
				if ( len( targetProperty.getValueResolutionFunction() ) )
				{
					var annotatedResolver = new PropertyResolver();
					var resolutionInstance = "";
					annotatedResolver.setTargetProperty( targetProperty.getName() );
					annotatedResolver.setSourceProperty( targetProperty.getSourcePropertyName() );
					resolutionInstance = createObject( "component", targetProperty.getValueResolver() );
					annotatedResolver.setValueResolutionTarget( resolutionInstance );
					annotatedResolver.setValueResolutionFunction( targetProperty.getValueResolutionFunction() );
					this.addPropertyResolver( annotatedResolver );
				}
			}
		}
		
		dequeueDelayedConfiguration();
	}

	/**
	 * Adds a property resolver from the given source property name to the target
	 * property name, stating that the passed valueResolutionFunction will be used
	 * to perform the value resolution.  To state that the entire source instance
	 * should be passed to the resolution function, pass null for the source property.
	 */
	public function addPropertyResolver( resolver )
	{
		if ( isNull( metadataBuilder ) )
		{
			arrayAppend( queuedPropertyResolvers, resolver );
		}
		else
		{
			var sourceProperties = metadataBuilder.buildMetadataForClass( source ).propertyMetadata;
			var targetProperties = metadataBuilder.buildMetadataForClass( target ).propertyMetadata;
				
			if ( not isNull(resolver.sourceProperty) && !sourceProperties.containsProperty( resolver.getSourceProperty() ) )
			{
				throw new Error( "Can't add a custom property resolver for the source property \"" + resolver.sourceProperty + "\" because no such property exists on the source class, " + Type.forClass( source ).fullName );
			}
			
			if ( !targetProperties.containsProperty( resolver.getTargetProperty() ) )
			{
				throw new Error( "Can't add a custom property resolver for the target property \"" + resolver.targetProperty + "\" because no such property exists on the target class, " + Type.forClass( target ).fullName );
			}
			
			var resolution = new PropertyResolution();
			
			if ( not isNull( resolver.getSourceProperty() ) ) resolution.setSourceProperty( sourceProperties.getProperty( resolver.getSourceProperty() ));
			resolution.setTargetProperty( targetProperties.getProperty( resolver.getTargetProperty() ) );
			resolution.setValueResolutionTarget( resolver.getValueResolutionTarget() );
			resolution.setValueResolutionFunction( resolver.getValueResolutionFunction() );
			resolution.setExplicit( true );
			
			resolvedProperties[ resolver.getTargetProperty() ] = resolution;
			
			if ( structKeyExistsStronglyCased( unresolvedPropertiesByName, resolver.getTargetProperty() ) )
			{
				structDelete( unresolvedPropertiesByName, resolver.getTargetProperty() );
			}
			
			if ( structKeyExistsStronglyCased( unresolvedPropertiesByType, resolver.getTargetProperty() ) )
			{
				structDelete( unresolvedPropertiesByType, resolver.getTargetProperty() );
			}
		}
		
		return this;
	}
	
	public function ignoreProperty( propertyName )
	{
		arrayAppend( ignoredProperties,  propertyName );
			
		return this;
	}
	
	public function setAggregatedType( propertyName, clazz )
	{
		if ( not isNull( metadataBuilder ) )
		{
			var targetProperties = metadataBuilder.buildMetadataForClass( target ).propertyMetadata;
			
			if ( !targetProperties.containsProperty( propertyName ) )
			{
				throw new Error( "Can't set the aggregated type for the target property \"" + propertyName + "\" because no such property exists on the target class, " + target.name );
			}
			
			aggregatedPropertyTargetTypes[ propertyName ] = getMetadata( clazz );
		}
		else
		{
			arrayAppend( queuedAggregates, { propertyName = propertyName, clazz = clazz } );
		}
		return this;
	}
	
	/**
	 * States if a given property (of the target class!) has been assigned
	 * a value resolution function.
	 */
	public function hasResolvedProperty( propertyName )
	{
		return structKeyExistsStronglyCased( resolvedProperties, propertyName );
	}
	
	/**
	 * States if a given property (of the target class!) has been assigned
	 * is missing a value resolution function.
	 */
	public function hasUnresolvedProperty( propertyName )
	{
		return structKeyExistsStronglyCased( unresolvedPropertiesByName, propertyName ) or structKeyExistsStronglyCased( unresolvedPropertiesByType, propertyName );
	}	
	

	
	/**
	 * Checks for unresolved propertyies by name and type, printing suggested resolutions
	 * to the console and throwing an error if unresolved properties are found.
	 */
	public function assertMappingIsValid() 
	{
		var name = [];
		var type = [];
		var err = "";
		var message = "MAPPING PROBLEMS: " & source.name & " -> " & source.name & "\n";
		var byName = "";
		var byType = "";
		var missingTypeAggregation = false;
		var targetProperty = "";
		var key = "";
		
		// If some properties type mapping relies on Mappings establish _after_ this one
		// was added to the mapper, we need to check their resolution.
		createLateMappedPropertyResolutions();
		
		for ( key in unresolvedPropertiesByName )
		{
			byName = unresolvedPropertiesByName[ key ];
			if ( not arrayFind( ignoredProperties, byName.getTargetProperty().getName() ) ) 
			{
				arrayAppend( name, byName );
					
				message &= byName.toString() & "\n";
			}
		}
		
		
		for ( key in unresolvedPropertiesByType )
		{
			byType = unresolvedPropertiesByName[ key ];
			if ( not arrayFind( ignoredProperties, byType.getTargetProperty().getName() ) ) 
			{
				arrayAppend( type, byType );
				
				message &= byType.toString() & "\n";
			}
		}

		// Validate our aggregated target types
		for ( key in aggregatedTargetProperties )
		{
			targetProperty = aggregatedTargetProperties[ key ];
			
			
			if ( !structKeyExistsStronglyCased( aggregatedPropertyTargetTypes, targetProperty.getName() ) 
				&& (
					!structKeyExistsStronglyCased( resolvedProperties, targetProperty.getName() )
					|| !resolvedProperties[ targetProperty.getName() ].getExplicit()
				)
			)
			{
				missingTypeAggregation = true;
					
				message &= "<p>-- MISSING AGGREGATED TYPE: For the target type " 
					& target.name & "'s """ & targetProperty.getName() & """</p><p>"
					& " property, you haven't stated what class to use to fill the collection.</p><p>"
					& " This means that the mapper can't map this property because it wouldn't know what types to create!</p><p>"
					& " Please call .setAggregatedType( """ & targetProperty.getName() & """, TypeName ) or add a manual property resolver to handle this property.</p>";
			}
		}
		
		if ( arrayLen(name) || arrayLen(type) > 0 || missingTypeAggregation )
		{
			writeLog( "INVALID MAPPINGS: " & message );
			var err = new InvalidMappingsError( name, type, message );
			err.throw();
		}		
		
		variables.validated = true;
			
		return true;
	}
	
	private boolean function structKeyExistsStronglyCased( map, key  )
	{
		return listFind( structKeyList( map ), key );
	}
	
}